ITU Graphics Programming

Exercise 08 - PBR scene viewer


Learning objectives

In this exercise we will learn the basics of the implementation of a physically-based material.

Remember that you can find hints in comments in the code, tagged with the label "(todo)" followed by the number of the exercise.


References


Exercise 08.0 - Get familiar with the new code in the library

Before we start implementing our PBR shaders, it is important to take a quick look to the new additions to the library:


Exercise 08.1 - Indirect diffuse

We need to replace the current Blinn-Phong shader program with a new one.

We are reading the material properties from textures that we will reuse in both. You already now the color and normal textures, but we will introduce a new one, ARM, named with the initials of the components packed in it:

We can reuse the same vertex shader, but we need some changes to the fragment shader:

  1. Duplicate the file default.frag and rename the new file to default_pbr.frag.
  2. In C++, locate where we load the fragment shader, and modify the path to using your new file.
  3. Modify also the path to the file we included with the shader model, replacing blinn-phong.glsl with lambert-ggx.glsl.
  4. Modify default_pbr.frag to adapt it to the SurfaceData required by lambert-ggx.glsl:
    1. Rename data.reflectionColor to data.albedo.
    2. Rename data.ambientReflectance to data.ambientOcclusion.
    3. Remove the lines assigning values to data.diffuseReflectance, data.specularReflectance and data.specularExponent.
    4. Initialize the property data.roughness to the value in the second channel of the ARM texture.
    5. Initialize the property data.metalness to the value in the third channel of the ARM texture.

If you run the program now, you should have a very similar result as before, but with only the ambient and diffuse terms.

Finally, let's replace the fixed ambient indirect diffuse lighting with something more PBR. One of the goals of PBR is to reduce the need to tweak materials when lighting conditions change. For this exercise, since we don't have any GI (global illumination), we will use the skybox to get the environment contribution.

In the lighting model library, lambert-ggx.glsl, inside the ComputeDiffuseIndirectLighting function, replace the fixed value (0.25f) with a sample from the environment texture, in the direction of the surface normal. You can use the helper function SampleEnvironment for this. For the LOD level (mipmap), we use the one with less detail, passing a value of 1 to the function.

You can set the light intensity to 0 to visualize only the contribution of the environment.

Try to load other models included in the exercise to validate the solution.


Exercise 08.2 - Indirect specular

In the Blinn-Phong lighting model all the indirect lighting is modelled by a single ambient value, with nothing specific for specular.

In this exercise we will add some environment reflection to model the indirect specular contribution, sampling from the skybox texture.

Find the ComputeSpecularIndirectLighting function in the lambert-ggx.glsl shader, and add a sample from the environment, using the same function as in the previous section.

For the direction, this time we won't use the normal map. Instead, find the reflected view vector on the normal of the surface. You can use the reflect GLSL function. The view direction used in lighting goes from the surface to the camera, but in this case you have to use the inverse. Use a LOD level of 0, for now.

If you did the calculation correctly, the object should look like a mirror! On thing required to improve this is to change the LOD level depending on the roughness.

Smooth surfaces will have a more detailed reflection (lower LOD level), while rough surfaces will have a blurred one (higher LOD level). You can use the roughness value, already in the 0-1 range, as the LOD level. However, because the mips have not been processed properly to contain smooth values, we will push the values towards 1. Use the following expression as your LOD level: pow(data.roughness, 0.25f).

It should look better now, but there is still obviously too much indirect lighting. We will deal with that in the next exercise.


Exercise 08.3 - Energy conservation (Fresnel term)

One of the principles of PBR is energy conservation. At the moment, we are adding the indirect light twice, as if it was reflected in all directions but also in the reflected direction (diffuse + specular).

To select between these two, we will use the Schlick approximation of the fresnel term. Look for the equation in the slides and implement it in lambert-ggx.glsl.

  1. In CombineIndirectLighting, compute the value of the fresnel, and use it to mix between diffuse and specular, instead of adding them together.
    (You can obtain the F0 value using the GetReflectance function. The 2 vectors that you need to provide to the function are the viewDir and the normal, instead of the halfDir.)
  2. Does it look better now? Thanks to the fresnel term, the indirect will be most visible when the view direction is more perpendicular to the normal.
  3. Follow the same procedure to mix diffuse and specular in CombineLighting, using the same reflectance, and the viewDir and halfDir vectors this time.


Exercise 08.4 - Diffuse (Lambert)

Now that the indirect lighting is working fine, we can move to direct lighting.

For the diffuse, we saw in the lecture some more complex models, but we will keep using the simple Lambertian model, adapted to PBR:

  1. As part of the general rendering equation, we have to move the angle of incidence out of the diffuse calculation in ComputeDiffuseLighting. Move it to the CombineLighting function, affecting the final result.
  2. Additionally, we need to normalize the amount of light, to be energy conserving. As we explained in the lecture, we need to divide by PI to account for all the different directions.
  3. The light of our previous scene was adjusted manually, which means that now it would be darker due to the division by PI. To solve this, we will change the light intensity of the directional light to 3, instead of one, to get a similar result.

The visual results should be very similar to the previous exercise.

This is a good time to try the different models included in the exercise and see how the light is affecting them.


Exercise 08.5 - Specular (Distribution function)

Until now, we didn't have any specular in this model. This is because the equations are completely different from the Blinn-Phong model, and it was not worth it to keep anything.

The first step towards our specular model is implementing the Cook-Torrance equation:

  1. Look at the slides and implement the Cook-Torrance equation in the ComputeSpecularLighting function. Skip the Fresnel term, which is already taken into account when combining with the diffuse.
  2. For the distribution term (D), call the DistributionGGX function.
  3. For the geometry term (G), call the GeometrySmith function.
  4. The dot products in the denominator can be computed with ClampedDot, as we are doing in other parts of the exercise.
  5. Finally, although it is not included in the original equation, add a small epsilon (0.00001f) to the denominator, to avoid infinity values when the dot products are very small.
  6. Make sure to return a vec3 value, even though all the factors are floats.

Both implementations of the D and G terms are initially empty. In this section of the exercise we will only implement the distribution function.

Find the DistributionGGX function and implement it, following the slides. The alpha factor used in the equation corresponds to our roughness.

You may notice some high specular values on some grazing angles. This looks incorrect, and will be solved by the geometry term in the next exercise section.


Exercise 08.6 - Specular (Geometry term)

As we discussed before, we need to implement the geometry term to account for shadowing and masking of the microsurfaces.

We are using the Smith equation that divides the G2 term in two separate G1 functions (one for shadowing, one for masking). This is already implemented for you, in the function GeometrySmith.

However, you still have to implement the equation for the G1 function for GGX, GeometrySchlickGGX. With the help of the slides, implement this equation and observe the results.

With this, the direct specular term is finally complete. But we can also improve the quality of the render by applying this same function to the indirect specular lighting. Multiply the specular indirect in ComputeSpecularIndirectLightingLighting with a call to GeometrySmith function, using reflectionDir as the input direction, instead of lightDir.

Our PBR model for dielectrics is finished now!


Exercise 08.7 - Metalness

There is only one small (but important) part to finish now.

Until now we have ignored metals completely, but they don't follow the same rendering model.

Metalness will affect two parts of our model:

Now we can say that the PBR model is finished. You should load all the different 3D model files in the exercise and look around them with the camera, observing the different materials.


Exercise 08.X - Get creative (optional)

The renderer we are using is too simple, and there are multiple options to improve it: